--[[
Copyright (C) 2008-2009 Adirelle

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--]]

--------------------------------------------------------------------------------
-- Addon declaration
--------------------------------------------------------------------------------

-- #DEFAULT_NODOC

Yatba = LibStub("AceAddon-3.0"):NewAddon("Yatba",
	"AceConsole-3.0", "AceEvent-3.0", "LibBars-1.0"
--[===[@debug@
	,"LibDebugLog-1.0"
--@end-debug@]===]
)
local Yatba = Yatba

--------------------------------------------------------------------------------
-- Globals made local for performance
--------------------------------------------------------------------------------

local pairs, ipairs, next, select, type, tonumber, tostring = pairs, ipairs, next, select, type, tonumber, tostring

--------------------------------------------------------------------------------
-- Libraries
--------------------------------------------------------------------------------

local L = LibStub("AceLocale-3.0"):GetLocale("Yatba")
Yatba.L = L

local LibBars = LibStub("LibBars-1.0")

local LibSharedMedia = LibStub("LibSharedMedia-3.0")
local MEDIA_FONT = LibSharedMedia.MediaType.FONT
local MEDIA_STATUSBAR = LibSharedMedia.MediaType.STATUSBAR

--------------------------------------------------------------------------------
-- Attributes
--------------------------------------------------------------------------------

Yatba.groups = {}
Yatba.bars = {}
Yatba.watchedGUIDs = {}

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

function Yatba.erase(...)
	for i=1,select('#', ...) do
		local t = select(i, ...)
		local	mt = getmetatable(t)
		setmetatable(t, nil)
		for k in pairs(t) do
			t[k] = nil
		end
		setmetatable(t, mt)
	end
end

do
	local list = setmetatable({}, {__mode='k'})
	function Yatba.new(...)
		local t = next(list)
		if t then
			list[t] = nil
			for i = 1, select('#', ...) do
				t[i] = select(i, ...)
			end
			return t
		else
			return {...}
		end
	end
	function Yatba.del(t)
		if type(t) == 'table' then
			setmetatable(t, nil)
			for k in pairs(t) do
				t[k] = nil
			end
			list[t] = true
		end
		return nil
	end
end

--@non-debug@
function Yatba:Debug() end
--@end-non-debug@

--------------------------------------------------------------------------------
-- Locals
--------------------------------------------------------------------------------

local db
local bars, groups, watchedGUIDs = Yatba.bars, Yatba.groups, Yatba.watchedGUIDs
local new, del, erase = Yatba.new, Yatba.del, Yatba.erase

--------------------------------------------------------------------------------
-- Internal signals
--------------------------------------------------------------------------------

Yatba.signals = LibStub('CallbackHandler-1.0'):New(Yatba, "RegisterSignal", "UnregisterSignal", "UnregisterAllSignals")

function Yatba:SendSignal(name, ...)
	self.signals:Fire(name, ...)
end

--------------------------------------------------------------------------------
-- Addon initializing and enabling
--------------------------------------------------------------------------------

function Yatba:OnInitialize()
	--[===[@debug@
	self.version = "r"..(("$Revision: 54 $"):match("%d+"))
	--@end-debug@]===]
	--@non-debug@
	self.version = tostring(GetAddOnMetadata("Yatba", "Version"))
	--@end-non-debug@

	self.db = LibStub("AceDB-3.0"):New("YatbaDB", {
		profile = {
			highlightTarget = false,
			highlightFocus = false,
			highlightTexture = 'Plain',
			statusbar = LibSharedMedia:GetDefault(MEDIA_STATUSBAR),
			font = LibSharedMedia:GetDefault(MEDIA_FONT),
			fontSize = 10,
			fontFlags = "",
			thickness = 15,
			headerHealth = true,
			healthGradient = false,
			showRaidIcons = true,
			colors = {
				--                  r    g    b    a
				buff            = { 0.0, 0.5, 1.0, 1.0 },
				debuff          = { 1.0, 0.5, 0.0, 1.0 },
				header          = { 1.0, 1.0, 1.0, 1.0 },
				lowHealth       = { 1.0, 0.0, 0.0, 1.0 },
				highHealth      = { 0.0, 1.0, 0.0, 1.0 },
				targetHighlight = { 1.0, 1.0, 0.0, 1.0 },
				focusHighlight  = { 1.0, 0.0, 1.0, 1.0 },
				debuffMagic     = { 0.2, 0.6, 1.0, 1.0 },
				debuffCurse     = { 0.6, 0.0, 1.0, 1.0 },
				debuffDisease   = { 0.6, 0.4, 0.0, 1.0 },
				debuffPoison    = { 0.0, 0.6, 0.0, 1.0 },
			},
			advancedFiltering = false,
			defaultRule = 'mine',
			auraRules = {},
			groups = {
				['**'] = {
					enable = true,
					length = 200,
					scale = 1.0,
					alpha = 1.0,
					orientation = LibBars.LEFT_TO_RIGHT,
					spacing = 0,
					reverseGrow = true,
					fill = false,
 					priority = 50,
					auraTypes = {
						['*'] = true;
					},
					units = {
						['*'] = true,
					},
					auraRules = {
						['*'] = 'mine',
					},
					durationFilter = true,
					minDuration = 0,
					maxDuration = 120,
					displayHeaders = true,
					textTemplate = "[$i ][$n: ]$s[ ($a)]",
				},
				Default = {
					isDefault = true,
					priority = 10,
					y=500,
					x=730,
				},
			}
		},

	})
	db = self.db.profile
	self:SendSignal('DatabaseSet')

	-- Register options and chat commands
	LibStub("AceConfig-3.0"):RegisterOptionsTable(self.name, self.options)
	self:RegisterChatCommand("yatba", "ChatCommand", true)

	-- Register profile options
	local profile_opts = LibStub('AceDBOptions-3.0'):GetOptionsTable(self.db)
	profile_opts.order = -10
	self.options.args.profile = profile_opts

	-- Create the LDB launcher
	LibStub('LibDataBroker-1.1'):NewDataObject(self.name, {
		type = 'launcher',
		icon = 'Interface\\Icons\\Spell_Shadow_LastingAfflictions',
		OnClick = function() self:OpenGUI() end,
	})

end

function Yatba:OnEnable()

	if not self.updateFrame then
		self.updateFrame = self:CreateUpdateFrame()
	end

	self.db.RegisterCallback(self, "OnNewProfile", "ReloadProfile")
	self.db.RegisterCallback(self, "OnProfileChanged", "ReloadProfile")
	self.db.RegisterCallback(self, "OnProfileCopied", "ReloadProfile")
	self.db.RegisterCallback(self, "OnProfileReset", "ReloadProfile")

	self:RegisterEvent("UNIT_TARGET")
	self:RegisterEvent("UNIT_PET")
	self:RegisterEvent("UNIT_AURA")
	self:RegisterEvent("UNIT_AURASTATE", "UNIT_AURA")
	self:RegisterEvent("PLAYER_FOCUS_CHANGED")
	self:RegisterEvent("PLAYER_TARGET_CHANGED")
	self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
	self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self:RegisterEvent("RAID_TARGET_UPDATE")

	self:RegisterEvent('PLAYER_DEAD', 'RefreshUnitTimers')

	self:RegisterEvent("PARTY_MEMBERS_CHANGED")
	self:RegisterEvent("RAID_ROSTER_UPDATE", "PARTY_MEMBERS_CHANGED")

	LibSharedMedia.RegisterCallback(self, "LibSharedMedia_SetGlobal", "OnMediaUpdated")
	self.RegisterSignal(self.name, 'ConfigChanged_statusbar', self.OnStatusBarUpdated, self)
	self.RegisterSignal(self.name, 'ConfigChanged_font', self.OnFontUpdated, self)
	self.RegisterSignal(self.name, 'ConfigChanged_fontSize', self.OnFontUpdated, self)
	self.RegisterSignal(self.name, 'ConfigChanged_fontFlags', self.OnFontUpdated, self)

	self:OnRosterUpdated()
	self:ReloadProfile()

	self:Debug('Yatba enabled')
end

function Yatba:OnDisable()
	if self.updateFrame then
		self.updateFrame:Hide()
	end
	self:SendSignal('AddonDisabled')
	for name,group in pairs(groups) do
		group:Hide()
	end
	self.UnregisterAllSignals(self.name)
	self:Debug('Yatba disabled')
end

function Yatba:OpenGUI()
	LibStub("AceConfigDialog-3.0"):Open(self.name)
end

function Yatba:ChatCommand(input)
	if not input or input:trim() == "" then
		self:OpenGUI()
	else
		LibStub("AceConfigCmd-3.0").HandleCommand(self, "yatba", self.name, input)
	end
end

function Yatba:OnFontUpdated()
	local font, size, flags = LibSharedMedia:Fetch(MEDIA_FONT, db.font), db.fontSize, db.fontFlags
	for name,group in pairs(groups) do
		group:SetFont(font, size, flags)
	end
end

function Yatba:OnStatusBarUpdated()
	local texture = LibSharedMedia:Fetch(MEDIA_STATUSBAR, db.statusbar)
	for name,group in pairs(groups) do
		group:SetTexture(texture)
	end
end

function Yatba:OnMediaUpdated(event, mediaType)
	if mediaType == MEDIA_FONT then
		self:OnFontUpdated()
	elseif mediaType == MEDIA_STATUSBAR then
		self:OnStatusBarUpdated()
	end
end

function Yatba.signals:OnUsed(target, name)
	if name:match('^HealthUpdated_') then
		Yatba:RegisterEvent("UNIT_HEALTH")
	end
end

function Yatba.signals:OnUnused(target, name)
	if name:match('^HealthUpdated_') then
		Yatba:UnregisterEvent("UNIT_HEALTH")
	end
end

function Yatba:ReloadProfile()

	-- Delete all aura options
	self:ResetAuraOptions()

	-- Remove any existing group and their bars
	for name,group in pairs(groups) do
		if group then
			self:ReleaseGroup(group)
		else
			groups[name] = nil
		end
	end

	-- Set the upvalue to the new profile
	db = self.db.profile
	self:SendSignal('DatabaseSet')

	-- (Re)build groups from database
	for name, def in pairs(db.groups) do
		if def then
			self:InitializeGroup(name, true)
			groups[name]:Show()
		end
	end

	-- Create aura options
	self:InitializeAuraOptions()

	self:RefreshUnitTimers()
end

function Yatba:SetLock(locked)
	for name,group in pairs(groups) do
		if locked then
			group:HideAnchor()
			group:Lock()
		else
			group:ShowAnchor()
			group:Unlock()
		end
	end
end

function Yatba:IsLocked()
	local n, locked, unlocked = 0, 0, 0
	for name,group in pairs(groups) do
		n = n + 1
		if group:IsLocked() then
			locked = locked + 1
		else
			unlocked = unlocked + 1
		end
	end
	if locked == n then
		return true
	elseif unlocked == n then
		return false
	else
		return
	end
end

CONFIGMODE_CALLBACKS = CONFIGMODE_CALLBACKS or {}
function CONFIGMODE_CALLBACKS.Yatba(action)
	if action == 'ON' then
		Yatba:SetLock(false)
	elseif action == 'OFF' then
		Yatba:SetLock(true)
	end
end

--------------------------------------------------------------------------------
-- Timer bar creation
--------------------------------------------------------------------------------

function Yatba:CreateTimerBar(GUID, unitId, auraName, auraType, auraCount, timeLeft, duration, texture, debuffType, isMine)
	if not unitId then return end
	local bestChoice, bestPriority = nil, -1
	for name, group in pairs(groups) do
		if group.db.enable and group.db.priority > bestPriority and group:AcceptTimer(unitId, auraName, auraType, duration, isMine) then
			bestChoice, bestPriority = group, group.db.priority
		end
	end
	if not bestChoice then
		self:Debug('Ignored %q on %q', auraName, UnitName(unitId))
		return
	end
	local group = bestChoice
	local displayName = UnitName(unitId)
	if UnitIsPlayer(unitId) then
		displayName = displayName:gsub('%-.+$', '')
	end
	self:Debug('Creating timer for %q in group %q', auraName, group.name)
	local bar = self.barPrototypes.timer:Create(group, GUID, unitId, displayName, auraName, auraType, auraCount, duration, timeLeft, texture, debuffType, isMine)
	group:SortBars()
	return bar
end

function Yatba:DropGUID(GUID)
	self:UnscheduleScan(GUID)
	if watchedGUIDs[GUID] then
		self:SendSignal('DropGUID_'..GUID)
	end
end

--------------------------------------------------------------------------------
-- Combat log analysis
--------------------------------------------------------------------------------

local COMBATLOG_OBJECT_AFFILIATION_MINE = COMBATLOG_OBJECT_AFFILIATION_MINE or 0x00000001

function Yatba:COMBAT_LOG_EVENT_UNFILTERED(_, timestamp, event, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, ...)

	-- We don't receive AURA_STATE events for mouseover, so monitor the combat log
	if (event:match('_AURA_APPLIED') or event:match('_AURA_REFRESH')) and dstGUID == UnitGUID('mouseover') then
		self:ScheduleScan(dstGUID)

	elseif watchedGUIDs[dstGUID] then

		if event == 'UNIT_DIED' then
			self:DropGUID(dstGUID)

		elseif event:match('_AURA_REMOVED') then
			self:SendSignal('DropAura_'..dstGUID..'_'..select(2, ...))
		end

	end

end

--------------------------------------------------------------------------------
-- Aura scanning
--------------------------------------------------------------------------------

do -- Yatba:ScanAuras

	local UnitBuff,UnitDebuff,UnitIsUnit = _G.UnitBuff,_G.UnitDebuff,_G.UnitIsUnit
	local function IsMeOrMine(caster)
		return caster and (UnitIsUnit(caster, 'player') or UnitIsUnit(caster, 'vehicle') or UnitIsUnit(caster, 'pet'))
	end
	local auraQueries = {
		BUFF = function(...)
			local name, rank, texture, count, buffType, duration, expirationTime, caster = UnitBuff(...)
			if name then
				return name, texture, count, duration or 0, expirationTime and (expirationTime-GetTime()) or 0, IsMeOrMine(caster)
			end
		end,
		DEBUFF = function(...)
			local name, rank, texture, count, debuffType, duration, expirationTime, caster = UnitDebuff(...)
			if name then
				return name, texture, count, duration or 0, expirationTime and (expirationTime-GetTime()) or 0, IsMeOrMine(caster), debuffType
			end
		end
	}

	local remaining = {}
	local ScanUnitAuras

	function ScanUnitAuras(self, unitId, GUID)
		assert(unitId and GUID)

		-- GUID changed or dead unit: stop here
		if UnitGUID(unitId) ~= GUID then
			return
		elseif UnitIsDeadOrGhost(unitId) then
			return self:DropGUID(dstGUID)
		end
		--self:Debug('Scanning %q -> %q', GUID, unitId)

		-- Update health
		self:SendSignal('HealthUpdated_'..GUID, unitId)

		-- Copy existing bars into a temporary table
		if watchedGUIDs[GUID] then
			for name, bar in pairs(watchedGUIDs[GUID]) do
				remaining[name] = bar
			end
		end

		-- Scan all auras
		for auraType,OurUnitAura in pairs(auraQueries) do
			for i=1,512 do
				local name,texture,count,duration,timeLeft,isMine,debuffType = OurUnitAura(unitId, i)
				if not name then
					break
				elseif duration > 0 and isMine then
					local bar = remaining[name]
					if not bar then
						Yatba:CreateTimerBar(GUID, unitId, name, auraType, count, timeLeft, duration, texture, debuffType, isMine)
					else
						bar:Refresh(timeLeft, duration, count, unitId)
						remaining[name] = nil
					end
				end
			end
		end

		-- Drop all remaining bars
		for name, bar in pairs(remaining) do
			bar:Die()
			remaining[name] = nil
		end
	end

	local updatePeriod = 1.0 / 25
	local rosterUpdated = false
	local guidToScan = {}
	local updateFrame
	local OnUpdate

	function OnUpdate(frame, elapsed)
		frame.timeToUpdate = frame.timeToUpdate - elapsed
		if frame.timeToUpdate > 0 then
			return
		end
		if rosterUpdated then
			Yatba:OnRosterUpdated()
			rosterUpdated = false
		end
		local now = GetTime()
		for GUID, unitId in pairs(guidToScan) do
			if unitId then
				ScanUnitAuras(Yatba, unitId, GUID)
				guidToScan[GUID] = nil
			end
		end
		frame:Hide()
	end

	function Yatba:CreateUpdateFrame()
		if not updateFrame then
			updateFrame = CreateFrame("Frame", "YatbaUpdateFrame")
			updateFrame:Hide()
			updateFrame:SetScript("OnUpdate", OnUpdate)
			updateFrame:SetScript("OnShow", function() updateFrame.timeToUpdate = updatePeriod end)
		end
		return updateFrame
	end

	function Yatba:ScheduleScan(GUID, unitId)
		if GUID and not guidToScan[GUID] then
			unitId = unitId or self:GetUnitIdForGUID(GUID)
			if unitId then
				--self:Debug('Scheduling scan for %q (%q)', GUID, unitId)
				guidToScan[GUID] = unitId
				updateFrame:Show()
			end
		end
	end

	function Yatba:UnscheduleScan(GUID)
		if GUID then
			guidToScan[GUID] = nil
		end
	end

	function Yatba:PARTY_MEMBERS_CHANGED()
		rosterUpdated = true
		updateFrame:Show()
	end

end

function Yatba:UNIT_AURA(event, unit)
	self:ScheduleScan(UnitGUID(unit), unit)
end

function Yatba:UPDATE_MOUSEOVER_UNIT()
	self:ScheduleScan(UnitGUID('mouseover'), 'mouseover')
end

function Yatba:UNIT_HEALTH(event, unitId)
	self:SendSignal('HealthUpdated_'..UnitGUID(unitId), unitId)
end

local commonUnits = {
	'player', 'pet', 'target', 'focus', 'mouseover',
	'pettarget', 'targettarget', 'focustarget', 'mouseovertarget'
}

do
	local iter

	function iter(t, unit)
		repeat
			unit = next(t.units, unit)
			if unit then
				local GUID = UnitGUID(unit)
				if GUID and not t.seen[GUID] then
					t.seen[GUID] = true
					return unit, GUID
				end
			end
		until not unit
		del(t.units, t.seen, t)
	end

	function Yatba:IterateUnits()
		local units = new()
		for i,unit in ipairs(commonUnits) do
			units[unit] = true
		end
		local n, prefix = GetNumRaidMembers(), 'raid'
		if not n or n == 0 then
			n, prefix = GetNumPartyMembers(), 'party'
		end
		for i = 1, n do
			units[prefix..i] = true
			units[prefix..'pet'..i] = true
			units[prefix..'target'..i] = true
		end
		local t = new()
		t.units = units
		t.seen = new()
		return iter, t
	end
end

function Yatba:RefreshUnitTimers()
	for unitId,GUID in self:IterateUnits() do
		self:ScheduleScan(GUID, unitId)
	end
end

function Yatba:RAID_TARGET_UPDATE()
	for unitId,GUID in self:IterateUnits() do
		self:SendSignal('UpdateRaidIcon', GUID, GetRaidTargetIndex(unitId))
	end
end

do -- Handle a mapping from GUIDs to UnitIds

	local playerGUID
	local petGUID
	local focusGUID
	local targetGUID
	local rosterMap = { unitToGUID = {}, guidToUnit = {} }
	local rosterPetMap = { unitToGUID = {}, guidToUnit = {} }
	local rosterTargetMap = { unitToGUID = {}, guidToUnit = {} }

	function GetUnitIdForGUID(self, GUID)
		return GUID and (
			(GUID == playerGUID and 'player') or
			(GUID == petGUID and 'pet') or
			rosterMap.guidToUnit[GUID] or
			rosterPetMap.guidToUnit[GUID] or
			(GUID == focusGUID and 'focus') or
			(GUID == targetGUID and 'target') or
			rosterTargetMap.guidToUnit[GUID] or
			(GUID == UnitGUID('mouseover') and 'mouseover')
		) or nil
	end
	Yatba.GetUnitIdForGUID = GetUnitIdForGUID

	local function UpdateGUIDMap(map, unitId)
		assert(unitId)
		local GUID = UnitGUID(unitId)
		local unitToGUID, guidToUnit = map.unitToGUID, map.guidToUnit
		local previousUnitId = GUID and guidToUnit[GUID]
		local previousGUID = unitToGUID[unitId]
		if previousUnitId == unitId and previousGUID == GUID then
			return
		end
		local isNew = not GetUnitIdForGUID(Yatba, GUID)
		if previousUnitId then unitToGUID[previousUnitId] = nil end
		if previousGUID then guidToUnit[previousGUID] = nil end
		if unitId then unitToGUID[unitId] = GUID end
		if GUID then
			guidToUnit[GUID] = unitId
			if isNew then
				Yatba:ScheduleScan(GUID, unitId)
			end
		end
	end

	function Yatba:OnRosterUpdated()
		erase(
			rosterMap.unitToGUID, rosterMap.guidToUnit,
			rosterPetMap.unitToGUID, rosterPetMap.guidToUnit,
			rosterTargetMap.unitToGUID, rosterTargetMap.guidToUnit
		)
		local prefix = 'party'
		local num = GetNumRaidMembers()
		if num > 0 then
			prefix = 'raid'
		else
			num = GetNumPartyMembers()
		end
		for i = 1, num do
			UpdateGUIDMap(rosterMap, prefix..i)
			UpdateGUIDMap(rosterPetMap, prefix..'pet'..i)
			UpdateGUIDMap(rosterTargetMap, prefix..i..'target')
		end
	end

	function Yatba:UNIT_PET(event, unit)
		local petUnitId = (unit ~= 'player' and unit or '')..'pet'
		local GUID = UnitGUID(petUnitId)
		if unit == 'player' then
			if GUID then
				self:ScheduleScan(GUID, petUnitId)
			elseif petGUID then
				self:SendSignal('DropGUID_'..petGUID)
			end
			petGUID = GUID
		else
			local oldPetGUID = rosterPetMap['__'..petUnitId]
			if not GUID and oldPetGUID then
				self:SendSignal('DropGUID_'..oldPetGUID)
			end
			UpdateGUIDMap(rosterPetMap, petUnitId)
		end
	end

	function Yatba:UNIT_TARGET(event, unit)
		if unit ~= "player" then
			UpdateGUIDMap(rosterTargetMap, unit..'target')
		end
	end

	function Yatba:PLAYER_FOCUS_CHANGED()
		focusGUID = UnitGUID('focus')
		self:ScheduleScan(focusGUID, 'focus')
		for name,group in pairs(groups) do
			group:SortBars()
		end
	end

	function Yatba:PLAYER_TARGET_CHANGED()
		targetGUID = UnitGUID('target')
		self:ScheduleScan(targetGUID, 'target')
		for name,group in pairs(groups) do
			group:SortBars()
		end
	end

	function Yatba:PLAYER_ENTERING_WORLD()
		playerGUID = UnitGUID('player')
		petGUID = UnitGUID('pet')
		focusGUID = UnitGUID('focus')
		targetGUID = UnitGUID('target')
		self:RefreshUnitTimers()
	end

end

